/*
 * $Id: PDFImage.java,v 1.2 2007/08/26 18:56:35 gil1 Exp $ $Date: 2007/08/26
 * 18:56:35 $ This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of the License,
 * or (at your option) any later version. This library is distributed in the
 * hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
 * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
 * the GNU Lesser General Public License for more details. You should have
 * received a copy of the GNU Lesser General Public License along with this
 * library; if not, write to the Free Software Foundation, Inc., 59 Temple
 * Place, Suite 330, Boston, MA 02111-1307 USA
 */
package pdf.handler;

import java.awt.Image;
import java.awt.image.ImageObserver;
import java.awt.image.PixelGrabber;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.Vector;
import java.util.zip.DeflaterOutputStream;

/**
 * <p>
 * This implements the Image XObject. Calling one of the <code>drawImage</code>
 * methods of <code>PDFGraphics</code> will put all the necessary code into the
 * pdf file, and the image will be encoded in ascii base 85, then deflated in
 * zip format.
 * </p>
 * 
 * @author Eric Z. Beard (original version by Peter Mount)
 * @author Matthew Hreljac, mhreljac@hotmail.com
 * @version $Revision: 1.2 $, $Date: 2007/08/26 18:56:35 $
 */
public class PDFImage extends PDFStream implements ImageObserver, Serializable
{
   /*
    * NOTE: The original class is the work of Peter T. Mount, who released it
    * in the uk.org.retep.pdf package. It was modified by Eric Z. Beard as
    * follows: The package name was changed to pdf.handler. The formatting was
    * changed a little bit. Images were not yet implemented, so the core of
    * this class was mostly rewritten It is still licensed under the LGPL. Got
    * some help with base85 methods from Mathew Hreljac
    */

   /**
    * <p>
    * Converts a pixel to a hex string
    * </p>
    * 
    * @param x
    *            an <code>int</code> value
    * @param y
    *            an <code>int</code> value
    * @param p
    *            an <code>int</code> value
    * @return a <code>String</code> value
    */
   public static String handlePixel(int x, int y, int p)
   {
      int red = (p >> 16) & 0xff;
      int green = (p >> 8) & 0xff;
      int blue = (p) & 0xff;
      String redHex = Integer.toHexString(red);
      String greenHex = Integer.toHexString(green);
      String blueHex = Integer.toHexString(blue);
      if (redHex.length() == 1)
         redHex = "0" + redHex;
      if (greenHex.length() == 1)
         greenHex = "0" + greenHex;
      if (blueHex.length() == 1)
         blueHex = "0" + blueHex;
      return redHex + greenHex + blueHex;
   } // end handlePixel
   private int height;
   private Image img;
   private String name;

   // Dimensions of the image.
   private int width;

   /**
    * Creates a new <code>PDFImage</code> instance.
    */
   public PDFImage()
   {
      super("/XObject");
   }

   /**
    * Creates a new <code>PDFImage</code> instance.
    * 
    * @param img
    *            an <code>Image</code> value
    */
   public PDFImage(Image img)
   {
      this();
      setImage(img, 0, 0, img.getWidth(this), img.getHeight(this), this);
   }

   /**
    * Creates a new <code>PDFImage</code> instance.
    * 
    * @param img
    *            an <code>Image</code> value
    * @param x
    *            an <code>int</code> value
    * @param y
    *            an <code>int</code> value
    * @param w
    *            an <code>int</code> value
    * @param h
    *            an <code>int</code> value
    * @param obs
    *            an <code>ImageObserver</code> value
    */
   public PDFImage(Image img, int x, int y, int w, int h, ImageObserver obs)
   {
      this();
      setImage(img, x, y, img.getWidth(this), img.getHeight(this), obs);
   }

   /**
    * <p>
    * Adobe's base 85 does not follow the format used by ipv6 addresses. It
    * simply starts with 33 and goes straight up without skipping any
    * characters
    * </p>
    * <p>
    * Parts of this method contributed by Mathew Hreljac
    * </p>
    * 
    * @param stringToEncode
    *            a <code>String</code> value
    * @return a <code>String</code> value
    */
   private String base85Encoding(String stringToEncode)
         throws NumberFormatException
         {
      if ((stringToEncode == null) || (stringToEncode.length() == 0))
         // System.out.println("PDFImage.base85Encoding() null or blank String");
         return "";
      if ((stringToEncode.length() > 8) ||
            ((stringToEncode.length() % 2) != 0))
      {
         System.out
         .println("PDFImage.base85Encoding, Incorrect tuple length: "
               +
               stringToEncode.length());
         return "";
      }
      // System.out.println("str: " + stringToEncode);
      // String buffer to use to return the String encoding
      StringBuffer sb = new StringBuffer();

      // Deal with a partial tuple (less than 8 hex digits)
      // From Adobe's docs:
      // "Given n (1, 2 or 3) bytes of binary data, the encoding first
      // appends 4 - n zero bytes to make a complete 4-tuple. This 4-tuple
      // is encoded in the usual way, but without applying the special
      // z-case. Finally, only the first n+1 characters of the resulting
      // 5-tuple are written out. Those characters are immediately followed
      // by the EOD marker, ~>"

      int numHexDigits = stringToEncode.length() / 2;
      int numAppendBytes = 4 - numHexDigits;
      for (int i = 0; i < numAppendBytes; i++)
         stringToEncode += "00";
      Vector<Integer> digitVector = new Vector<Integer>();
      long number = Long.parseLong(stringToEncode, 16);
      int remainder = 0;

      while (number >= 85)
      {
         remainder = (int) (number % 85);
         number = number / 85;
         digitVector.add(0, new Integer(remainder));
      }
      digitVector.add(0, new Integer((int) number));

      for (int i = 0; i < digitVector.size(); i++)
      {
         char c = (char) (digitVector.elementAt(i).intValue() + 33);
         sb.append(c);
      }
      String tuple = sb.toString();
      int len = tuple.length();
      switch (len)
      {
      case 1:
         tuple = "!!!!" + tuple;
         break;
      case 2:
         tuple = "!!!" + tuple;
         break;
      case 3:
         tuple = "!!" + tuple;
         break;
      case 4:
         tuple = "!" + tuple;
         break;
      default:
         break;
      } // end switch
      // System.out.println("enc tuple: " + tuple);

      return (tuple);
         } // end base85encoding

   /**
    * Get the value of height.
    * 
    * @return value of height.
    */
   public int getHeight()
   {
      return height;
   }

   /**
    * Get the name
    * 
    * @return a <code>String</code> value
    */
   public String getName()
   {
      return name;
   }

   /**
    * Get the value of width.
    * 
    * @return value of width.
    */
   public int getWidth()
   {
      return width;
   }

   /**
    * Describe <code>imageUpdate</code> method here.
    * 
    * @param img
    *            an <code>Image</code> value
    * @param infoflags
    *            an <code>int</code> value
    * @param x
    *            an <code>int</code> value
    * @param y
    *            an <code>int</code> value
    * @param w
    *            an <code>int</code> value
    * @param h
    *            an <code>int</code> value
    * @return a <code>boolean</code> value
    */
   @Override
   public boolean imageUpdate(Image img, int infoflags, int x, int y, int w,
         int h)
   {
      System.err.println("img=" + img + "\ninfoflags=" + infoflags +
            "\nx=" + x + " y=" + y + " w=" + w + " h=" + h);
      // if(img == this.img) {
      if (infoflags == ImageObserver.WIDTH)
         width = w;
      if (infoflags == ImageObserver.HEIGHT)
         height = h;

      // return true;
      // }
      return false;
   }

   /**
    * Set the value of height.
    * 
    * @param v
    *            Value to assign to height.
    */
   public void setHeight(int v)
   {
      this.height = v;
   }

   /**
    * Set the image
    * 
    * @param img
    *            an <code>Image</code> value
    * @param x
    *            an <code>int</code> value
    * @param y
    *            an <code>int</code> value
    * @param w
    *            an <code>int</code> value
    * @param h
    *            an <code>int</code> value
    * @param obs
    *            an <code>ImageObserver</code> value
    */
   public void setImage(Image img, int x, int y, int w, int h,
         ImageObserver obs)
   {
      this.img = img;
      width = w;
      height = h;
   }

   /**
    * Set the name
    * 
    * @param n
    *            a <code>String</code> value
    */
   public void setName(String n)
   {
      name = n;
   }

   /**
    * Set the value of width.
    * 
    * @param v
    *            Value to assign to width.
    */
   public void setWidth(int v)
   {
      this.width = v;
   }

   /**
    * <p>
    * Compression needs to be improved here
    * </p>
    * 
    * @param os
    *            OutputStream to send the object to
    * @exception IOException
    *                on error
    */
   @Override
   public void write(OutputStream os) throws IOException
   {
      writeStart(os);

      // write the extra details
      os.write("/Subtype /Image\n/Name ".getBytes());
      os.write(name.getBytes());
      os.write("\n/Width ".getBytes());
      os.write(Integer.toString(width).getBytes());
      os.write("\n/Height ".getBytes());
      os.write(Integer.toString(height).getBytes());
      os.write("\n/BitsPerComponent 8\n/ColorSpace /DeviceRGB\n".getBytes());

      // write the pixels to the stream
      // System.err.println("Processing image "+width+"x"+height+" pixels");
      ByteArrayOutputStream bos = getStream();

      int w = width;
      int h = height;
      int x = 0;
      int y = 0;
      int[] pixels = new int[w * h];
      PixelGrabber pg = new PixelGrabber(img, x, y, w, h, pixels, 0, w);
      try
      {
         pg.grabPixels();
      }
      catch (InterruptedException e)
      {
         System.err.println("interrupted waiting for pixels!");
         return;
      }
      if ((pg.getStatus() & ImageObserver.ABORT) != 0)
      {
         System.err.println("image fetch aborted or errored");
         return;
      }
      StringBuffer out = new StringBuffer();
      for (int j = 0; j < h; j++)
         for (int i = 0; i < w; i++)
         {
            // System.out.print("p[" + j * w + i+ "]=" + pixels[j * w + i] +
            // ".");
            out.append(handlePixel(x + i, y + j, pixels[j * w + i]));
            if (out.toString().length() >= 8)
            {
               String tuple = out.substring(0, 8);
               out.delete(0, 8);
               // Convert !!!!! to 'z'
               String encTuple = base85Encoding(tuple);
               if (encTuple.equals("!!!!!"))
                  encTuple = "z";
               bos.write(encTuple.getBytes());
            }
         }

      String lastTuple = base85Encoding(out.toString());
      // System.out.println("lastTuple: " + lastTuple);
      bos.write(lastTuple.getBytes());
      bos.write("~".getBytes());

      // System.out.println("Processing done");

      // this will write the actual stream
      setDeflate(false);

      writeStream(os);

      // Note: we do not call writeEnd() on streams!
   }

   /**
    * Writes the image to the stream
    * 
    * @param os
    *            an <code>OutputStream</code> value
    * @exception IOException
    *                if an error occurs
    */
   @Override
   public void writeStream(OutputStream os) throws IOException
   {
      // This is a non-deflated stream
      /*
       * os.write("/Length ".getBytes()); // Accout for stream\n ...
       * >\nendstream os.write(Integer.toString(buf.size() + 18).getBytes());
       * os.write("\n/Filter /ASCII85Decode".getBytes());
       * os.write("\n>>\nstream\n".getBytes()); buf.writeTo(os);
       * os.write(">\nendstream\nendobj\n\n".getBytes());
       */
      ByteArrayOutputStream b = new ByteArrayOutputStream();
      DeflaterOutputStream dos = new DeflaterOutputStream(b);
      buf.writeTo(dos);
      dos.finish();
      dos.close();

      // FlatDecode is compatible with the java.util.zip.Deflater class
      // os.write("/Filter [/FlateDecode /ASCIIHexDecode]\n".getBytes());
      os.write("/Filter [/FlateDecode /ASCII85Decode]\n".getBytes());
      os.write("/Length ".getBytes());
      os.write(Integer.toString(b.size()).getBytes());
      os.write("\n>>\nstream\n".getBytes());
      b.writeTo(os);
      os.write("\nendstream\nendobj\n".getBytes());

   } // end writeStream

} // end class PDFImage
